﻿using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using Microsoft.Win32;

namespace MaterialDesignThemes.Wpf;

///<summary>
/// Represents a display device or multiple display devices on a single system.
/// Based on http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Screen.cs
/// </summary>
internal class Screen
{
    private static class NativeMethods
    {
        private const string User32 = "user32.dll";

        [DllImport(User32, ExactSpelling = true, CharSet = CharSet.Auto)]
        [ResourceExposure(ResourceScope.None)]
        public static extern int GetSystemMetrics(int nIndex);

        [DllImport(User32, CharSet = CharSet.Auto)]
        [ResourceExposure(ResourceScope.None)]
        public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX info);

        [DllImport(User32, ExactSpelling = true)]
        [ResourceExposure(ResourceScope.None)]
        public static extern bool EnumDisplayMonitors(HandleRef hdc, COMRECT? rcClip, MonitorEnumProc lpfnEnum, IntPtr dwData);

        [DllImport(User32, ExactSpelling = true)]
        [ResourceExposure(ResourceScope.None)]
        public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, int flags);

        [DllImport(User32, ExactSpelling = true)]
        [ResourceExposure(ResourceScope.None)]
        public static extern IntPtr MonitorFromRect(ref RECT rect, int flags);

        public delegate bool MonitorEnumProc(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lParam);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
        public class MONITORINFOEX
        {
            internal int cbSize = Marshal.SizeOf(typeof(MONITORINFOEX));
            internal RECT rcMonitor = new RECT();
            internal RECT rcWork = new RECT();
            internal int dwFlags = 0;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
            internal char[] szDevice = new char[32];
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;

            public RECT(Rect r)
            {
                left = (int)r.Left;
                top = (int)r.Top;
                right = (int)r.Right;
                bottom = (int)r.Bottom;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public class COMRECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct POINTSTRUCT
        {
            public int x;
            public int y;
            public POINTSTRUCT(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        }

        public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero);

        public const int SM_CMONITORS = 80;
    }

    private readonly IntPtr _hmonitor;

    /// <summary>
    /// Available working area on the screen. This excludes taskbars and other
    /// docked windows.
    /// </summary>
    private Rect _workingArea = Rect.Empty;

    private static readonly object _syncLock = new object();//used to lock this class before sync'ing to SystemEvents

    private static int _desktopChangedCount = -1;//static counter of desktop size changes

    private int _currentDesktopChangedCount = -1;//instance-based counter used to invalidate WorkingArea

    // This identifier is just for us, so that we don't try to call the multimon
    // functions if we just need the primary monitor... this is safer for
    // non-multimon OSes.
    private const int PRIMARY_MONITOR = unchecked((int)0xBAADF00D);

    private const int MONITOR_DEFAULTTONEAREST = 0x00000002;
    private const int MONITORINFOF_PRIMARY = 0x00000001;

    private static readonly bool _multiMonitorSupport = NativeMethods.GetSystemMetrics(NativeMethods.SM_CMONITORS) != 0;
    private static Screen[]? _screens;

    private Screen(IntPtr monitor)
    {
        if (!_multiMonitorSupport || monitor == (IntPtr)PRIMARY_MONITOR)
        {
            // Single monitor system
            Bounds = new Rect(SystemParameters.VirtualScreenLeft, SystemParameters.VirtualScreenTop,
                SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight);
            Primary = true;
            DeviceName = "DISPLAY";
        }
        else
        {
            // MultiMonitor System
            // We call the 'A' version of GetMonitorInfoA() because
            // the 'W' version just never fills out the struct properly on Win2K.
            NativeMethods.MONITORINFOEX info = new NativeMethods.MONITORINFOEX();
            NativeMethods.GetMonitorInfo(new HandleRef(null, monitor), info);
            Bounds = new Rect(info.rcMonitor.left, info.rcMonitor.top, info.rcMonitor.right - info.rcMonitor.left, info.rcMonitor.bottom - info.rcMonitor.top);
            Primary = (info.dwFlags & MONITORINFOF_PRIMARY) != 0;

            DeviceName = new string(info.szDevice);
            DeviceName = DeviceName.TrimEnd((char)0);

        }
        _hmonitor = monitor;
    }

    /// <summary>
    /// Gets an array of all of the displays on the system.
    /// </summary>
    public static Screen[]? AllScreens
    {
        get
        {
            if (_screens == null)
            {
                if (_multiMonitorSupport)
                {
                    MonitorEnumCallback closure = new MonitorEnumCallback();
                    NativeMethods.MonitorEnumProc proc = closure.Callback;
                    NativeMethods.EnumDisplayMonitors(NativeMethods.NullHandleRef, null, proc, IntPtr.Zero);

                    if (closure.Screens.Count > 0)
                    {
                        Screen[] temp = new Screen[closure.Screens.Count];
                        closure.Screens.CopyTo(temp, 0);
                        _screens = temp;
                    }
                    else
                    {
                        _screens = new[] { new Screen((IntPtr)PRIMARY_MONITOR) };
                    }
                }
                else
                {
                    if (PrimaryScreen is Screen screen)
                    {
                        _screens = new[] { screen };
                    }
                    else
                    {
                        _screens = null;
                    }
                }

                // Now that we have our screens, attach a display setting changed
                // event so that we know when to invalidate them.
                SystemEvents.DisplaySettingsChanging += OnDisplaySettingsChanging;
            }

            return _screens;
        }
    }

    /// <summary>
    /// Gets the bounds of the display.
    /// </summary>
    public Rect Bounds { get; }

    /// <summary>
    /// Gets the device name associated with a display.
    /// </summary>
    public string DeviceName { get; }

    /// <summary>
    /// Gets a value indicating whether a particular display is the primary device.
    /// </summary>
    public bool Primary { get; }

    /// <summary>
    /// Gets the primary display.
    /// </summary>
    public static Screen? PrimaryScreen
    {
        get
        {
            if (_multiMonitorSupport)
            {
                foreach (Screen screen in AllScreens ?? Enumerable.Empty<Screen>())
                {
                    if (screen.Primary)
                    {
                        return screen;
                    }
                }
                return null;
            }
            return new Screen((IntPtr)PRIMARY_MONITOR);
        }
    }

    /// <summary>
    /// Gets the working area of the screen.
    /// </summary>
    public Rect WorkingArea
    {
        get
        {

            //if the static Screen class has a different desktop change count 
            //than this instance then update the count and recalculate our working area
            if (_currentDesktopChangedCount != DesktopChangedCount)
            {
                Interlocked.Exchange(ref _currentDesktopChangedCount, DesktopChangedCount);

                if (!_multiMonitorSupport || _hmonitor == (IntPtr)PRIMARY_MONITOR)
                {
                    // Single monitor system
                    _workingArea = SystemParameters.WorkArea;
                }
                else
                {
                    // MultiMonitor System
                    // We call the 'A' version of GetMonitorInfoA() because
                    // the 'W' version just never fills out the struct properly on Win2K.
                    NativeMethods.MONITORINFOEX info = new NativeMethods.MONITORINFOEX();
                    NativeMethods.GetMonitorInfo(new HandleRef(null, _hmonitor), info);
                    _workingArea = new Rect(info.rcWork.left, info.rcWork.top, info.rcWork.right - info.rcWork.left, info.rcWork.bottom - info.rcWork.top);
                }
            }
            return _workingArea;
        }
    }

    /// <summary>
    /// Screen instances call this property to determine
    /// if their WorkingArea cache needs to be invalidated.
    /// </summary>
    private static int DesktopChangedCount
    {
        get
        {
            if (_desktopChangedCount == -1)
            {

                lock (_syncLock)
                {

                    //now that we have a lock, verify (again) our change count...
                    if (_desktopChangedCount == -1)
                    {
                        //sync the UserPreference.Desktop change event.  We'll keep count 
                        //of desktop changes so that the WorkingArea property on Screen 
                        //instances know when to invalidate their cache.
                        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;

                        _desktopChangedCount = 0;
                    }
                }
            }
            return _desktopChangedCount;
        }
    }

    /// <summary>
    /// Specifies a value that indicates whether the specified object is equal to this one.
    /// </summary>
    public override bool Equals(object? obj) => obj is Screen comp && _hmonitor == comp._hmonitor;

    /// <summary>
    /// Retrieves a <see cref='Screen'/> for the monitor that contains the specified point.
    /// </summary>
    public static Screen FromPoint(Point point)
    {
        if (_multiMonitorSupport)
        {
            NativeMethods.POINTSTRUCT pt = new NativeMethods.POINTSTRUCT((int)point.X, (int)point.Y);
            return new Screen(NativeMethods.MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST));
        }
        return new Screen((IntPtr)PRIMARY_MONITOR);
    }

    /// <summary>
    /// Retrieves a <see cref='Screen'/> for the monitor that contains the largest region of the Rect.
    /// </summary>
    public static Screen FromRect(Rect rect)
    {
        if (_multiMonitorSupport)
        {
            NativeMethods.RECT rc = new NativeMethods.RECT(rect);
            return new Screen(NativeMethods.MonitorFromRect(ref rc, MONITOR_DEFAULTTONEAREST));
        }
        return new Screen((IntPtr)PRIMARY_MONITOR);
    }

    ///<summary>
    /// Retrieves the working area for the monitor that is closest to the specified point.
    /// </summary>
    public static Rect GetWorkingArea(Point pt) => FromPoint(pt).WorkingArea;

    ///<summary>
    /// Retrieves the working area for the monitor that contains the largest region of the specified Rect.
    /// </summary>
    public static Rect GetWorkingArea(Rect rect) => FromRect(rect).WorkingArea;

    ///<summary>
    /// Retrieves the bounds of the monitor that is closest to the specified point.
    /// </summary>
    public static Rect GetBounds(Point pt) => FromPoint(pt).Bounds;

    /// <summary>
    /// Retrieves the bounds of the monitor that contains the largest region of the specified Rect.
    /// </summary>
    public static Rect GetBounds(Rect rect) => FromRect(rect).Bounds;

    /// <summary>
    /// Computes and retrieves a hash code for an object.
    /// </summary>
    public override int GetHashCode() => (int)_hmonitor;

    /// <summary>
    /// Called by the SystemEvents class when our display settings are
    /// changing.  We cache screen information and at this point we must
    /// invalidate our cache.
    /// </summary>
    private static void OnDisplaySettingsChanging(object? sender, EventArgs e)
    {
        // Now that we've responded to this event, we don't need it again until
        // someone re-queries. We will re-add the event at that time.
        SystemEvents.DisplaySettingsChanging -= OnDisplaySettingsChanging;

        // Display settings changed, so the set of screens we have is invalid.
        _screens = null;
    }

    /// <summary>
    /// Called by the SystemEvents class when our display settings have
    /// changed.  Here, we increment a static counter that Screen instances
    /// can check against to invalidate their cache.
    /// </summary>
    private static void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        if (e.Category == UserPreferenceCategory.Desktop)
        {
            Interlocked.Increment(ref _desktopChangedCount);
        }
    }

    /// <summary>
    /// Retrieves a string representing this object.
    /// </summary>
    public override string ToString() => GetType().Name + "[Bounds=" + Bounds + " WorkingArea=" + WorkingArea + " Primary=" + Primary + " DeviceName=" + DeviceName;

    private class MonitorEnumCallback
    {
        public List<Screen> Screens { get; } = new List<Screen>();

        public virtual bool Callback(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lparam)
        {
            Screens.Add(new Screen(monitor));
            return true;
        }
    }
}
